/****************************************************************
 ** Product  :   HP Subscriber Network Application Policy
 ** Module   :   gr-manager
 ** Date: Apr 10, 2015               
 ** Author: Huang xiaoji
 ** (C) Copyright 2015, Hewlett-Packard Company, All Rights Reserved.
 ** This software is the proprietary information of HP, Inc.  
 ** Use is subject to license terms.
 *****************************************************************
 ** Description:   
 **    Replace file content
 **    
 ****************************************************************/
package com.hp.snap.gr.action.impl;

import com.hp.snap.gr.exception.ActionException;
import com.hp.snap.gr.model.jaxb.ActionType;
import com.hp.snap.gr.model.jaxb.FileReplaceActionType;
import com.hp.snap.gr.model.jaxb.ReplaceType;
import com.hp.snap.gr.utils.Constants;

import org.apache.commons.codec.digest.DigestUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.FileCopyUtils;

import java.io.*;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * Replace file content
 * Some parameter can be configured in the action item: file,replace,include,ignoreContent and exclude.
 * 
 * file: File or directory to replace.
 * replace: replace content configuration, Multiple value.
 * include: include file name pattern, '*' and '?' can be used, Multiple value.
 * ignoreContent: provide a way to avoid replace special content in file.
 * exclude: exclude file name pattern, same as include.
 * 
 * <P>
 * Version: 3.3 <br>
 * Author: Huang xiaoji Date: Apr 10, 2015
 * 
 * </P>
 * 
 **/
public class FileReplaceActionHandler extends AbstractActionHandler {
    public static final int DIRECT_FROM_TO = 1;
    public static final int DIRECT_TO_FROM = 2;

    private static Logger logger = LoggerFactory.getLogger(FileReplaceActionHandler.class);

    @Override
    public void execute(ActionType action,
                        Map<String, Object> actionPlanInputs,
                        Map<String, Object> actionPlanOutputs) throws ActionException {
        logger.debug("invoke the FileReplaceAction");
        if (action instanceof FileReplaceActionType) {
            FileReplaceActionType actionType = (FileReplaceActionType) action;

            File f = new File(actionType.getFile());
            if (!f.exists()) throw new ActionException("File " + f.getAbsolutePath() + " does not exists");

            if (f.isDirectory()) {
                for (File file : f.listFiles()) {
                    if (doFilter(file, actionType)) {
                        doFile(file, actionType, DIRECT_FROM_TO);
                        doFile(file, actionType, DIRECT_TO_FROM);
                    }
                }
            } else {
                doFile(f, actionType, DIRECT_FROM_TO);
                doFile(f, actionType, DIRECT_TO_FROM);
            }
        }
    }

    private boolean doFilter(File file, FileReplaceActionType actionType) {
        if (!actionType.getExclude().isEmpty()) {
            for (String exclude : actionType.getExclude()) {
                Pattern p = Pattern.compile(exclude.replaceAll("\\.", "\\.").replaceAll("\\*", ".*").replaceAll("\\?", ".?"));
                if (p.matcher(file.getName()).find()) return false;
            }
        }
        if (!actionType.getInclude().isEmpty()) {
            for (String include : actionType.getInclude()) {
                Pattern p = Pattern.compile(include.replaceAll("\\.", "\\.").replaceAll("\\*", ".*").replaceAll("\\?", ".?"));
                if (p.matcher(file.getName()).find()) return true;
            }
            return false;
        } else {
            return true;
        }
    }

    private void doFile(File f, FileReplaceActionType actionType, int direct) throws ActionException {
        if (logger.isDebugEnabled()) {
            logger.debug("file replace {} {} started...", f.getAbsolutePath(), direct == DIRECT_FROM_TO ? "FROM -> TO" : "TO -> FROM");
        }
        if (direct == DIRECT_TO_FROM) {
            File org = new File(Constants.USER_GR_TEMP, f.getName() + ".org");
            InputStream orgIS = null;
            InputStream fIS = null;
            try {
                orgIS = new FileInputStream(org);
                fIS = new FileInputStream(f);
                String md5org = DigestUtils.md5Hex(orgIS);
                String md5rs = DigestUtils.md5Hex(fIS);

                if (!md5org.equalsIgnoreCase(md5rs)) {
                    logger.debug("Applied FROM -> TO, skip replace TO -> FROM");
                    return;
                }
            } catch (Exception e) {
                logger.error(e.getMessage(), e);
                throw new ActionException(e);
            } finally {
                InputStream[] isArr = {orgIS, fIS};
                for ( InputStream is : isArr) {
                    try {
                        is.close();
                    } catch (IOException e) {
                        logger.warn("Cannot release {"+ is + "}.");
                    } 
                }
            }
        }
        BufferedReader reader = null;
        FileOutputStream output = null;
        InputStream fReaderIs = null;
        InputStreamReader insReader = null;
        try {
            fReaderIs = new FileInputStream(f);
            FileCopyUtils.copy(f, new File(Constants.USER_GR_TEMP, f.getName() + ".org"));

            insReader =new InputStreamReader(fReaderIs);
            reader = new BufferedReader(insReader);
            File tmp = File.createTempFile("tmp", "file");
            output = new FileOutputStream(tmp);
            try {
                FileContentFilter contentFilter = new FileContentFilter(actionType);
                while (reader.ready()) {
                    String line = reader.readLine();
                    contentFilter.filter(line);
                }

                for (int i = 0; i < contentFilter.getLines().size(); i++) {
                    String line = contentFilter.getLines().get(i);
                    if (contentFilter.canReplace(i)) {
                        for (ReplaceType r : actionType.getReplace()) {
                            String org = null;
                            if (logger.isTraceEnabled()) {
                                org = line;
                            }
                            if (direct == DIRECT_FROM_TO)
                                line = line.replaceAll(r.getFrom(), r.getTo());
                            else
                                line = line.replaceAll(r.getTo(), r.getFrom());
                            if (logger.isTraceEnabled()) {
                                if (org != null && !org.equals(line)) {
                                    logger.trace("applied replace {} -> {} on line: {}", new Object[] {
                                            direct == DIRECT_FROM_TO ? r.getFrom() : r.getTo(),
                                            direct == DIRECT_FROM_TO ? r.getTo() : r.getFrom(), line });
                                }
                            }
                        }
                    }
                    output.write(line.getBytes());
                    output.write(Constants.CRLF);
                }
                output.flush();
            } finally {
                if (output != null) output.close();
                if (reader != null) reader.close();
            }

            FileCopyUtils.copy(tmp, f);

            tmp.delete();
        } catch (FileNotFoundException e) {
            logger.error(e.getMessage(), e);
            throw new ActionException(e);
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
            throw new ActionException(e);
        } finally {
            try {
                if (insReader != null)
                    insReader.close();
            } catch (IOException e) {
                logger.warn("Cannot release {" + insReader + "}.");
            }
            InputStream[] isArr = { fReaderIs };
            for (InputStream is : isArr) {
                try {
                    is.close();
                } catch (IOException e) {
                    logger.warn("Cannot release {" + is + "}.");
                }
            }
            
        }
    }
}

class FileContentFilter {
    private static final Logger logger = LoggerFactory.getLogger(FileContentFilter.class);

    List<contentFilterProcessor> processors;
    List<String> lines;
    List<Boolean> results;

    FileContentFilter(FileReplaceActionType action) {
        processors = new LinkedList<contentFilterProcessor>();
        for (FileReplaceActionType.IgnoreContent content : action.getIgnoreContent()) {
            if (content.getLine() != null) {
                processors.add(new LineMatchProcessor(content.getLine().getPattern()));
                if (logger.isDebugEnabled()) {
                    logger.debug("add line match processor, pattern: {}", content.getLine().getPattern());
                }
            } else if (content.getBlock() != null) {
                processors.add(new BlockMatchProcessor(content.getBlock().getBlockStartPattern(),
                        content.getBlock().getBlockEndPattern(), content.getBlock().getMatchPattern()));
                if (logger.isDebugEnabled()) {
                    logger.debug("add block match processor, start pattern: {}, end pattern: {}, match pattern: {}",
                            new Object[]{content.getBlock().getBlockStartPattern(), content.getBlock().getBlockEndPattern(), content.getBlock().getMatchPattern()});
                }
            }
        }
        lines = new LinkedList<String>();
        results = new LinkedList<Boolean>();
    }

    void filter(String line) {
        lines.add(line);
        results.add(true);

        for (contentFilterProcessor processor : processors) {
            if (processor.filter(lines.size() - 1, line)) {
                if (processor.isDenied()) {
                    if (logger.isTraceEnabled()) {
                        logger.trace("from line {} to {} has been filter by processor {}",
                                new Object[]{processor.getStartLine(), processor.getEndLine(), processor.getClass()});
                    }
                    for (int i = processor.getStartLine(); i <= processor.getEndLine(); i++) {
                        results.set(i, false);
                    }
                }
            }
        }
    }

    List<String> getLines() {
        return lines;
    }

    boolean canReplace(int index) {
        return results.get(index);
    }
}

interface contentFilterProcessor {
    boolean filter(int index, String line);

    int getStartLine();

    int getEndLine();

    boolean isDenied();
}

class LineMatchProcessor implements contentFilterProcessor {
    Pattern pattern;
    int lineIndex = -1;
    boolean denied = false;

    LineMatchProcessor(String pattern) {
        this.pattern = Pattern.compile(pattern);
    }

    @Override
    public boolean filter(int index, String line) {
        this.lineIndex = index;
        denied = pattern.matcher(line).find();
        return true;
    }

    @Override
    public int getStartLine() {
        return lineIndex;
    }

    @Override
    public int getEndLine() {
        return lineIndex;
    }

    @Override
    public boolean isDenied() {
        return denied;
    }
}

class BlockMatchProcessor implements contentFilterProcessor {
    Pattern startPattern, endPattern, matchPattern;

    boolean blockstarted = false;
    int start, end;
    int nextstart = -1;
    boolean nextdenied = false;
    boolean denied = false;

    BlockMatchProcessor(String startPattern, String endPattern, String matchPattern) {
        this.startPattern = Pattern.compile(startPattern);
        this.endPattern = endPattern != null ? Pattern.compile(endPattern) : null;
        this.matchPattern = Pattern.compile(matchPattern);
    }


    @Override
    public boolean filter(int index, String line) {
        if (!blockstarted) {
            if (startPattern.matcher(line).find()) {
                blockstarted = true;
                start = index;
                denied = matchPattern.matcher(line).find();
                return false;
            } else {
                start = index;
                end = index;
                denied = false;
                return true;
            }
        } else {
            if (endPattern != null) {
                if (endPattern.matcher(line).find()) {
                    blockstarted = false;
                    end = index;
                    if (!denied) {
                        denied = matchPattern.matcher(line).find();
                    }
                    return true;
                }
            } else {
                if (nextstart >= 0) {
                    start = nextstart;
                    denied = nextdenied;
                    nextstart = -1;
                }
                if (startPattern.matcher(line).find()) {
                    end = index - 1;
                    nextstart = index;
                    nextdenied = matchPattern.matcher(line).find();
                    return true;
                }
            }
        }
        return false;
    }

    @Override
    public int getStartLine() {
        return start;
    }

    @Override
    public int getEndLine() {
        return end;
    }

    @Override
    public boolean isDenied() {
        return denied;
    }
}
